# -*- coding: utf-8 -*-


# cmap2owl -- Helper application to convert from concept maps to OWL ontologies
# Copyright (c) 2008-2013  Rodrigo Rizzi Starr
#  
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#  
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#  
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


'''
OWL serialization interface

@author: Rodrigo Rizzi Starr
@copyright: Copyright © 2008-2013 Rodrigo Rizzi Starr
@license: MIT License
@contact: rodrigo.starr@gmail.com
'''


#from mapper import Mapper
from CMap2Prolog import assemblyQuery
from xml.dom import XML_NAMESPACE
import lxml.etree as et
from log import log
log.debug('Loaded...')


class OWLSerializable:
    '''An interface that will be accessed by the class OWLSerializer to generate
    an OWL element for certain rule types.'''
    
    def __init__(self):
        pass
    
    def getOWLString(self):
        '''Should return a XML string with this element representation'''
        pass


# Due to the way lxml treat namespaces, we will use these helper functions to
# generate qualified names.

# Set some alias for useful namespaces
XML_NS = unicode(XML_NAMESPACE)
RDF_NS = u'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
OWL_NS = u'http://www.w3.org/2002/07/owl#'
XSD_NS = u'http://www.w3.org/2001/XMLSchema#'
RDFS_NS = u'http://www.w3.org/2000/01/rdf-schema#'
PART_NS = u'http://www.w3.org/2001/sw/BestPractices/OEP/SimplePartWhole/part.owl'

def _genNamespaceWrapper(namespace):
    return lambda x: unicode('{' + namespace + '}' + x)

_xml = _genNamespaceWrapper(XML_NS)
_rdf = _genNamespaceWrapper(RDF_NS)
_owl = _genNamespaceWrapper(OWL_NS)
_xsd = _genNamespaceWrapper(XSD_NS)
_rdfs = _genNamespaceWrapper(RDFS_NS)
_part = _genNamespaceWrapper(PART_NS)

    
class OWLSerializer:
    '''Writes an OWL abstract representation of the classes in the prolog 
    database of a Mapper class into a file'''
     
    def __init__(self, fileName, mapper):
        
        self.fileName = fileName
        self.db = mapper.db
        self.root = None
        
        # This is a list of functions to be called to generate the XML for each 
        # OWL construct
        self.handlers = [self.classHandler,
                         self.individualHandler,
                         self.relationHandler,
                         self.transitivityHandler,
                         ]
        
    def serialize(self):
        "Scans the mapper and gets every OWL construct in there"
        
        self.createRoot()
        for handler in self.handlers:
            handler()
            
        tree = et.ElementTree(self.root)
        with open(self.fileName, 'w') as f:
            tree.write(f, 
                       encoding=u"utf-8", 
                       xml_declaration=True,
                       pretty_print=True,
                )
        
    def createRoot(self):
        '''Generates the root element and the starting part of an OWL file'''
        
        self.root = et.Element(_rdf('RDF'), nsmap={u'rdf': RDF_NS,
                                                   u'owl': OWL_NS,
                                                   u'xsd': XSD_NS,
                                                   u'rdfs': RDFS_NS})
        
        ontologyTag = et.Element(_owl('Ontology'),
                                 attrib={_rdf('about'): u''})
        self.writeMereologyImport(ontologyTag)
        
        self.root.append(ontologyTag)
    
    def writeMereologyImport(self, ontologyTag):
        '''Wites the import statement for the base part-whole ontology'''
        
        query = 'hasPartClassesOWL(Class, ClassSet)'
        
        if len(list(self.db.query(query))) == 0:
            return
        
        importPart = et.Element(_owl('imports'),
                                attrib={_rdf('resource'): PART_NS})
        ontologyTag.append(importPart)
    
    def writeLabel(self, element, label, lang='pt-BR'):
        'Write a label element'
        
        labelElement = et.Element(_rdfs('label'),
                                  attrib={_xml('lang'): unicode(lang)})
        labelElement.text = unicode(label)
        
        element.append(labelElement)
        
    def writeAboutClass(self, element, about):
        'Write a label element'
        aboutElement = et.Element(_owl('Class'),
                                  attrib={_rdf('about'): u'#' + unicode(about)})
        element.append(aboutElement)
        
    def createUnionOf(self, element, elList):
        '''Creates a unionOf anonymous class if necessary, that is, if elList is
        bigger than 1'''
        
        if len(elList) > 1:
            # Creates the anonymous class
            classElement = et.Element(_owl('Class'))
            unionOfElement = et.Element(_owl('unionOf'),
                                        attrib={_rdf('parseType'): u'Collection'})
            classElement.append(unionOfElement)
            element.append(classElement)
            
            # Now inserts the components
            for component in elList:
                self.writeAboutClass(unionOfElement, unicode(component))
        else:
            self.writeAboutClass(element, unicode(elList[0]))
    
    # Handlers
    def classHandler(self):
        'Generates a concept OWL element'
        
        query = "classOWL(Concept)," \
                "label(Concept, Label)"
        visited = set([])
        
        for concept in list(self.db.query(query)):
            conceptID = unicode(concept['Concept'])
            label = unicode(concept['Label'])
            
            # Avoid repeated elements
            if conceptID not in visited:
                visited.add(conceptID)
                
                conceptElement = et.Element(_owl('Class'),
                                            attrib={_rdf('ID'): conceptID})
                self.root.append(conceptElement)
                
                self.writeLabel(conceptElement, label)
                self.writeEnumeration(conceptElement, conceptID)
                self.writeSubclass(conceptElement, conceptID)
#                self.writeDisjunction(conceptElement, conceptID)
                self.writeHasPart(conceptElement, conceptID)
                
    def individualHandler(self):
        'Generates an owl element for an individual'
        
        query = 'individualOWL(Individual),' \
                'label(Individual, Label)'
        visited = set([])
        
        for individual in list(self.db.query(query)):
            individualId = unicode(individual['Individual'])
            label = unicode(individual['Label'])
        
            # Avoid repeated elements
            if individualId not in visited:
                visited.add(individualId)
                
                individualElement = et.Element(_owl('Thing'),
                                               attrib={_rdf('ID'): individualId})
                self.root.append(individualElement)
        
                self.writeLabel(individualElement, label)
                self.writeType(individualElement, individualId)
                self.writeIndividualProperties(individualElement, individualId)
#                self.writeDifferentFrom(individualElement, individualId)
            
    def relationHandler(self):
        'Generates an owl element for a relation'
        
        query = 'relationOWL(Relation), ' \
                'label(Relation, Label)'
        visited = set([])
        
        for relation in list(self.db.query(query)):
            
            relationId = unicode(relation['Relation'])
            label = unicode(relation['Label'])
        
            # Avoid repeated elements
            if relationId not in visited:
                visited.add(relationId)
                
                oPropertyElement = et.Element(_owl('ObjectProperty'),
                                              attrib={_rdf('ID'): relationId})
                self.root.append(oPropertyElement)
        
                self.writeLabel(oPropertyElement, label)
                self.domainRangeComplement(oPropertyElement, relationId)
                self.inverseOfRelation(oPropertyElement, relationId)
                self.functionalRelation(oPropertyElement, relationId)
            
    def transitivityHandler(self):
        'Generates the info about a transitive property.'
        
        query = 'transitivePropertyOWL(Relation)'
        visited = set([])
        
        for relation in list(self.db.query(query)):
            relationId = unicode(relation['Relation'])
        
            # Avoid repeated elements
            if relationId not in visited:
                visited.add(relationId)
                
                tpElement = et.Element(_owl('TransitiveProperty'),
                                       attrib={_rdf('about'): u'#' + relationId})
                self.root.append(tpElement)
        
    def domainRangeComplement(self, propertyElement, owlRelation):
        'Generates the domain and range for an OWL property'
        
        query = assemblyQuery('relationOWLDomain(%s, Domain)', owlRelation)
        
        try:
            relation = list(self.db.query(query))[0]
            # Opens the tags for the domain
            domainElement = et.Element(_rdfs('domain'))
            self.createUnionOf(domainElement, relation['Domain'])
            propertyElement.append(domainElement)
        except IndexError:
            pass
            
        
        query = assemblyQuery('relationOWLRange(%s, Range)', owlRelation)
        try:
            relation = list(self.db.query(query))[0]
            # And now the range
            rangeElement = et.Element(_rdfs('range'))
            self.createUnionOf(rangeElement, relation['Range'])
            propertyElement.append(rangeElement)
        except IndexError:
            pass
            
    def inverseOfRelation(self, propertyElement, relation):
        '''Checks if a relation has an inverse of property and write it
        accordingly'''
        
        query = assemblyQuery('inverseOfOWL(%s, Relation)', relation)
        
        try:
            relation = list(self.db.query(query))[0]
            relation = unicode(relation['Relation'])
        except IndexError:
            return
            
        # Opens the tags for the domain
        inverseOfElement = et.Element(_owl('inverseOf'),
                                     attrib={_rdf('resource'): u'#' + relation})
        propertyElement.append(inverseOfElement)
    
    def functionalRelation(self, propertyElement, relation):
        '''Checks if a relation has an inverse of property and write it
        accordingly'''
        
        query = assemblyQuery('functionalOWL(%s)', relation)
        
        try:
            relation = list(self.db.query(query))[0]
            relation = unicode(relation)
        except IndexError:
            return
            
        # Opens the tags for the relation
        fpElement = et.Element(_rdf('type'),
                               attrib={_rdf('resource'):
                                       OWL_NS + u'FunctionalProperty'})
        propertyElement.append(fpElement)
    
    def writeEnumeration(self, classElement, concept):
        '''Check if the element is composed of an enumeration and inserts it'''
        
        query = assemblyQuery('enumerationOWL(%s, Set)', concept)
        
        try:
            concept = list(self.db.query(query))[0]
            enumSet = concept['Set']
        except IndexError:
            return

        # Opens the tags for the domain
        oneOfElement = et.Element(_owl('oneOf'),
                                  attrib={_rdf('parseType'): u'Collection'})
        for el in enumSet:
            thingElement = et.Element(_owl('Thing'),
                                      attrib={_rdf('about'): u'#' + unicode(el)})
            oneOfElement.append(thingElement)
            
        classElement.append(oneOfElement)
        
    def writeDisjunction(self, element, concept):
        '''Check if the class is disjoint to any other'''
        
        query = assemblyQuery('disjunctionOWL(Set), member(%s, Set)', concept)
        
        for disjunctionSet in list(self.db.query(query)):
            classes = disjunctionSet['Set']
            
            for el in (klass for klass in classes if str(klass) != concept):
                # Opens the tags for the domain
                et.Element(_owl('disjointWith'),
                            attrib={(_rdf('resource'), self.rdf): u'#' + 
                                        unicode(el)})
                self.writer.endElement(_owl('disjointWith'))

    def writeSubclass(self, element, concept):
        '''Check if the class is subclass of another'''
        
        query = assemblyQuery('subclassOWL(%s, Superclass)', concept)
        
        for superClass in list(self.db.query(query)):
            superClass = superClass['Superclass']
            subclassElement = et.Element(_rdfs('subClassOf'),
                                         attrib={_rdf('resource'): u'#' + 
                                                 unicode(superClass)})
            element.append(subclassElement)

    def writeType(self, element, individual):
        '''Writes the types of an individual'''
        
        query = assemblyQuery('classOfIndividualOWL(%s, Class)', individual)
        
        for superClass in list(self.db.query(query)):
            superClass = superClass['Class']
            superClassElement = et.Element(_rdf('type'),
                                           attrib={_rdf('resource'): u'#' + 
                                                   unicode(superClass)})
            element.append(superClassElement)

    def writeDifferentFrom(self, element, individual):
        'Generates the info about different groups.'
        
        query = assemblyQuery('allDifferentOWL(Set), member(%s, Set)',
                              individual)
        
        for allDifferentSet in list(self.db.query(query)):
            classes = allDifferentSet['Set']
            
            for el in (klass for klass in classes if str(klass) != individual):
                # Opens the tags for the domain
                differFromElement = et.Element(_owl('differentFrom'),
                                               attrib={_rdf('resource'): u'#' + 
                                                       unicode(el)})
                element.append(differFromElement)
        
    def writeIndividualProperties(self, element, individual):
        '''Write the asserted properties about an individual'''
        
        query = assemblyQuery('relationOWLIndividual(Relation, %s, Ito)', 
                              individual)
        
        relations = list(self.db.query(query))

        # Writes the relations
        for data in relations:
            relElement = et.Element(unicode(data['Relation']),
                                    attrib={_rdf('resource'):
                                            u'#' + unicode(data['Ito'])})
            element.append(relElement)
            
        
    def writeHasPart(self, element, concept):
        '''Checks if the class is composed of others'''
        
        query = assemblyQuery(
                          '%s = Class, '
                            'hasPartClassesOWL(Class, ClassSet), '
                            '\+ hasPartSufficientClassesOWL(Class, ClassSet)',
                          concept)
        
        answerSet = list(self.db.query(query))
        
        if len(answerSet) > 0:
            
            subClassElement = et.Element(_rdfs('subClassOf'))
            element.append(subClassElement)
            
            restrictionElement = et.Element(_owl('Restriction'))
            subClassElement.append(restrictionElement)
            
            onPropertyElement = et.Element(_owl('onProperty'),
                                           attrib={_rdf('resource'): 
                                                   PART_NS + u'#hasPart_directly'})
            restrictionElement.append(onPropertyElement)
            
            svfElement = et.Element(_owl('someValuesFrom'))
            restrictionElement.append(svfElement)
            
            classes = []
            for answer in answerSet:
                classes.extend([str(atom) for atom in answer['ClassSet']])
            self.createUnionOf(svfElement, classes)

        # Now search for sufficient hasParts
        query = assemblyQuery('%s = Class, '
                              'hasPartClassesOWL(Class, ClassSet), '
                              'hasPartSufficientClassesOWL(Class, ClassSet)',
                              concept)
        
        answerSet = list(self.db.query(query))
        
        if len(answerSet) > 0:
            
            equivClassElement = et.Element(_owl('equivalentClass'))
            element.append(equivClassElement)
            
            restrictionElement = et.Element(_owl('Restriction'))
            equivClassElement.append(restrictionElement)
            
            onPropertyElement = et.Element(_owl('onProperty'),
                                           attrib={_rdf('resource'): 
                                                   PART_NS + u'#hasPart_directly'})
            restrictionElement.append(onPropertyElement)
            
            svfElement = et.Element(_owl('someValuesFrom'))
            restrictionElement.append(svfElement)
            
            classes = []
            for answer in answerSet:
                classes.extend([str(atom) for atom in answer['ClassSet']])
            self.createUnionOf(svfElement, classes)

